/*
 ------------------------------------------------------------------------------
 Copyright (C) 2006-2007 Team Blur.

 This file is part of the Quantum Engine source code.

 The Quantum Engine source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The Quantum Engine source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the Quantum Engine source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


// cl_main.cpp  -- client main loop


#include "client.h"


clientActive_t		cl;
clientStatic_t		cls;

cvar_t *			cl_aviDemo;
cvar_t *			cl_forceAviDemo;
cvar_t *			cl_timeNudge;
cvar_t *			cl_showTimeDelta;
cvar_t *			cl_showMessages;
cvar_t *			cl_showFPS;
cvar_t *			cl_playerName;
cvar_t *			cl_password;
cvar_t *			cl_rate;
cvar_t *			cl_snapshots;
cvar_t *			cl_maxPackets;
cvar_t *			cl_packetDup;
cvar_t *			cl_allowDownload;
cvar_t *			cl_deltaCompress;
cvar_t *			cl_timeOut;
cvar_t *			cl_rconAddress;
cvar_t *			cl_rconPassword;

cvar_t *			in_freeLook;
cvar_t *			in_alwaysRun;
cvar_t *			in_pitchSpeed;
cvar_t *			in_yawSpeed;
cvar_t *			in_angleSpeedKey;

cvar_t *			m_showRate;
cvar_t *			m_showMove;
cvar_t *			m_sensitivity;
cvar_t *			m_smooth;
cvar_t *			m_accel;
cvar_t *			m_pitch;
cvar_t *			m_yaw;
cvar_t *			m_strafe;


/*
 ==================
 CL_LoadMedia
 ==================
*/
static void CL_LoadMedia (void){

	// Get renderer and sound system information
	R_GetGLConfig(&cls.glConfig);
	S_GetALConfig(&cls.alConfig);

	// Load the materials required for console and cinematic drawing
	cls.whiteMaterial = R_RegisterMaterialNoMip("white");
	cls.consoleMaterial = R_RegisterMaterialNoMip("console");
	cls.charsetMaterial = R_RegisterMaterialNoMip("charset");
	cls.cinematicMaterial = R_RegisterMaterialNoMip("cinematic");
}

/*
 ==================
 CL_FullRestart

 Called by CL_RestartVideo_f and CL_RestartSound_f to perform a full restart if
 needed (to reload the whole level)
 ==================
*/
static void CL_FullRestart (void){

	if (cls.state < CS_LOADING)
		return;

	// Free the level
	CG_FreeLevel();

	// Shutdown GUI
	GUI_Shutdown();

	// Shutdown sound system and renderer
	S_Shutdown(true);
	R_Shutdown(true);

	// Initialize renderer and sound system
	R_Init(true);
	S_Init(true);

	// Initialize GUI
	GUI_Init();

	// Load local media
	CL_LoadMedia();

	// Load the level
	CG_LoadLevel();
}

/*
 ==================
 CL_InitAll

 Called before loading a level or after disconnecting from the server to
 initialize the necessary subsystems
 ==================
*/
void CL_InitAll (void){

	if (cls.state == CS_UNINITIALIZED)
		return;		// Nothing running on the client

	// Initialize renderer and sound system
	R_Init(false);
	S_Init(false);

	// Initialize GUI
	GUI_Init();

	// Load local media
	CL_LoadMedia();
}

/*
 ==================
 CL_ShutdownAll

 Called before loading a level or after disconnecting from the server to shut
 down the necessary subsystems
 ==================
*/
void CL_ShutdownAll (void){

	if (cls.state == CS_UNINITIALIZED)
		return;		// Nothing running on the client

	// Shutdown GUI
	GUI_Shutdown();

	// Shutdown sound system and renderer
	S_Shutdown(false);
	R_Shutdown(false);
}

/*
 ==================
 CL_Disconnect

 Sends a disconnect message to the server and goes to the main menu GUI or a
 fullscreen console.
 This is also called when an error is thrown, so it shouldn't cause any errors.
 ==================
*/
void CL_Disconnect (bool restart){
	if (cls.state == CS_DISCONNECTED){
		return;
	}

	// Send a disconnect message to the server (send it a few times in case one
	// is dropped)
	if (cls.state >= CS_CONNECTED){
		CL_SendReliableCommand("disconnect");
	}

	// Shutdown game module
	CG_Shutdown();

	// Wipe the entire clientActive_t structure
	memset(&cl, 0, sizeof(clientActive_t));

	// Clear key and mouse input states
	Key_ClearStates();
	IN_ClearStates();

	// Force console off
	Con_Close();

	// Free the clip map
	CM_FreeMap();

	// Allow cheats
	CVar_FixCheatVariables(true);

	// Restart the subsystems if desired
	if (cls.state >= CS_CONNECTED && restart){
		CL_ShutdownAll();

		FS_Restart(false, NULL);

		CL_InitAll();
	}

	// We are fully disconnected from the server
	cls.state = CS_DISCONNECTED;
}

/*
 ==================
 CL_Drop

 Called after a non-fatal error was thrown
 ==================
*/
void CL_Drop (const char *message){

	if (cls.state == CS_UNINITIALIZED)
		return;		// Nothing running on the client

	CL_Disconnect(true);

	// TODO: also set message for GUI to let the user know about it
}

/*
 ==================
 CL_LevelLoading

 The local server is starting to load a level, so connect the client
 ==================
*/
void CL_LevelLoading (void){

	if (cls.state == CS_UNINITIALIZED)
		return;		// Nothing running on the client

	// If we're already connected to the local server, stay connected
	if (cls.state >= CS_CONNECTED){
		cls.state = CS_CONNECTED;

		// Shutdown game module
		CG_Shutdown();

		return;
	}

	// If connected to a server, disconnect
	CL_Disconnect(false);

	// Connect to the local server
	Str_Copy(cls.serverName, "localhost", sizeof(cls.serverName));

	//NET_StringToNetAdr(cls.serverName, &clc.serverAddress);

	// CL_CheckForResend will fire immediately
	//cls.state = CS_CHALLENGING;

	//clc.connectTime = cls.frameTime - (RESEND_TIMEOUT * 2);
	//clc.connectCount = 0;
	//clc.connectRejected = false;
}

/*
 ==================
 CL_SendReliableCommand

 The given command will be sent to the server, and is guaranteed to not have
 any future userCmd_t processed before it is executed
 ==================
*/
void CL_SendReliableCommand (const char *fmt, ...){

	char	command[MAX_STRING_LENGTH];
	va_list	argPtr;
	int		len;
	ByteBuffer msg;
	byte buf[MAX_MSGLEN];
	msg.Init(buf,MAX_MSGLEN,false);
	if (cls.state < CS_CONNECTED)
		return;

	va_start(argPtr, fmt);
	len = Str_VSPrintf(command, sizeof(command), fmt, argPtr);
	va_end(argPtr);

	if (len == -1)
		Com_Error(false, "CL_SendReliableCommand: command string overflow");

	//send a reliable packet
	msg.WriteByte(CLC_COMMAND);
	msg.WriteData(command,len);
	ENetPacket * packet = enet_packet_create (msg.data,msg.getBytesWritten(),ENET_PACKET_FLAG_RELIABLE);
    enet_peer_send (cls.server, 0, packet);

	// If we would be losing an old command that hasn't been acknowledged, we
	// must drop the connection
	//if (clc.reliableCommandSequence - clc.reliableCommandAcknowledge == MAX_RELIABLE_COMMANDS)
	//	Com_Error(false, "Overflowed reliable client command buffer");

	// Store it off
	//clc.reliableCommandSequence++;

	//index = clc.reliableCommandSequence & (MAX_RELIABLE_COMMANDS - 1);
	//Str_Copy(clc.reliableCommandList[index], command, sizeof(clc.reliableCommandList[index]));
}

/*
 ==================
 CL_ForwardCommandToServer

 Adds the current command line as a reliable command.
 Things like god, noclip, etc, are commands directed to the server, so when
 they are typed in at the console, they will need to be forwarded.
 ==================
*/
void CL_ForwardCommandToServer (void){

	const char	*cmd;

	cmd = Cmd_Argv(0);

	if (cls.state != CS_ACTIVE || (*cmd == '+' || *cmd == '-')){
		Com_Printf("Unknown command '%s'\n", cmd);
		return;
	}

	CL_SendReliableCommand("%s", Cmd_Args(0, -1, false));
}

/*
 ==================
 CL_VerifyClientAtServer
 ==================
*/
static void CL_VerifyClientAtServer (void){

	ulong	packChecksums[MAX_PACK_FILES];
	char	checksums[MAX_STRING_LENGTH];
	int		i;

	// Build a string of pack checksums for pure validation on the server
	FS_LoadedPackChecksums(packChecksums);

	checksums[0] = 0;

	for (i = 0; i < MAX_PACK_FILES; i++){
		if (!packChecksums[i])
			break;

		Str_Append(checksums, Str_VarArgs("%u ", packChecksums[i]), sizeof(checksums));
	}

	// Ask the server to verify our game protocol and pack checksums.
	// If everything is right, the server will send us the game state so we can
	// finally load the level.
	CL_SendReliableCommand("verifyClient %i \"%s\"", cg->gameProtocol, checksums);
}

/*
 ==================
 CL_CheckUserInfo
 ==================
*/
static void CL_CheckUserInfo (void){

	if (cls.state < CS_CONNECTED)
		return;

	// Send a reliable user info update if needed
	if (CVar_GetModifiedFlags() & CVAR_USERINFO){
		CVar_ClearModifiedFlags(CVAR_USERINFO);

		CL_SendReliableCommand("userInfo \"%s\"", CVar_UserInfoString());
	}
}

/*
 ==================
 CL_CheckForResend

 Resend a connection request message if the last one has timed out
 ==================
*/
/*
static void CL_CheckForResend (void){

	if (cls.state != CS_CONNECTING && cls.state != CS_CHALLENGING)
		return;

	// Don't spam the server with connection requests if already rejected
	if (clc.connectRejected)
		return;

	// Resend if we haven't gotten a reply yet
	if (cls.frameTime - clc.connectTime < RESEND_TIMEOUT)
		return;

	clc.connectTime = cls.frameTime;
	clc.connectCount++;

	switch (cls.state){
	case CS_CONNECTING:
		// Request a challenge
		NET_OutOfBandPrint(NS_CLIENT, clc.serverAddress, "getChallenge");

		break;
	case CS_CHALLENGING:
		// Request a connection
		CVar_ClearModifiedFlags(CVAR_USERINFO);

		NET_OutOfBandPrint(NS_CLIENT, clc.serverAddress, "connect %i %i %i \"%s\"", PROTOCOL_VERSION, CVar_GetVariableInteger("net_clientPort"), clc.serverChallenge, CVar_UserInfoString());

		break;
	}
}
*/

// ============================================================================


/*
 ==================
 CL_Connect_f
 ==================
*/
static void CL_Connect_f (void){

	if (Cmd_Argc() != 2){
		Com_Printf("Usage: connect <server>\n");
		return;
	}

	Str_Copy(cls.serverName, Cmd_Argv(1), sizeof(cls.serverName));

	// If running a local server, kill it
	//SV_Shutdown("Server quit");

	// If connected to a server, disconnect
	CL_Disconnect(true);

	Com_Printf("Connecting to %s...\n", cls.serverName);
	
	ENetAddress address;
	enet_address_set_host (& address, cls.serverName);
    address.port = PORT_SERVER;
    /* Initiate the connection, allocating the two channels 0 and 1. */
    cls.server = enet_host_connect (cls.client, & address, 1);    
    if (cls.server == NULL)
		Com_Printf("No available peers for initiating an ENet connection.\n");
}

/*
 ==================
 CL_Disconnect_f
 ==================
*/
static void CL_Disconnect_f (void){

	if (cls.state == CS_DISCONNECTED){
		Com_Printf("Not connected to a server\n");
		return;
	}

	Com_Error(false, "Disconnected from server");
}

/*
 ==================
 CL_UserInfo_f
 ==================
*/
static void CL_UserInfo_f (void){

	const char	*infoString;
	char		*ch;

	infoString = CVar_UserInfoString();

	Com_Printf("User info settings:\n");
	Com_Printf("-------------------\n");

	while (1){
		ch = Str_FindChar(infoString, '\\');
		if (!ch)
			break;

		*ch++ = 0;

		// Print the name/value pair
		Cmd_TokenizeString(infoString);

		Com_Printf("%-20s %s\n", Cmd_Argv(0), Cmd_Args(1, -1, false));

		// Advance
		infoString = (const char *)ch;
	}
}

/*
 ==================
 CL_DumpServerInfo_f
 ==================
*/
static void CL_DumpServerInfo_f (void){

	const char	*infoString;
	char		*ch;

	if (cls.state < CS_CONNECTED){
		Com_Printf("Not connected to a server\n");
		return;
	}

	infoString = cl.serverInfo;

	Com_Printf("Server info settings:\n");
	Com_Printf("---------------------\n");

	while (1){
		ch = Str_FindChar(infoString, '\\');
		if (!ch)
			break;

		*ch++ = 0;

		// Print the name/value pair
		Cmd_TokenizeString(infoString);

		Com_Printf("%-20s %s\n", Cmd_Argv(0), Cmd_Args(1, -1, false));

		// Advance
		infoString = (const char *)ch;

		*--ch = '\\';
	}
}

/*
 ==================
 CL_RestartVideo_f
 ==================
*/
static void CL_Restart_f (void){

	// If needed, perform a full restart so the level is reloaded
	if (cls.state >= CS_LOADING){
		CL_FullRestart();
		return;
	}

	// Shutdown GUI
	GUI_Shutdown();

	// Restart the renderer
	R_Shutdown(true);
	R_Init(true);

	// Initialize GUI
	GUI_Init();

	// Load local media
	CL_LoadMedia();
}

// ============================================================================

void CL_ReadPackets(){
	ENetEvent ev;
    ByteBuffer msg;
	byte buf[MAX_MSGLEN];
	msg.Init(buf,MAX_MSGLEN,false);
	if(!cls.client)
		return;
    /* Wait up to 1000 milliseconds for an event. */
    while (enet_host_service (cls.client, & ev, 1) > 0)
    {
        switch (ev.type)
        {
        case ENET_EVENT_TYPE_CONNECT:
            Com_Printf("Connection succeeded.\n");
			cls.state=CS_CONNECTED;
            break;

        case ENET_EVENT_TYPE_RECEIVE:
			//if(ev.peer!=cls.server)
			//	break;//not from our server
            msg.ClearWriteState();
			msg.ClearReadState();
			msg.WriteData(ev.packet->data,ev.packet->dataLength);
			CL_ParseServerMessage(&msg);
            enet_packet_destroy (ev.packet);
            break;
           
        case ENET_EVENT_TYPE_DISCONNECT:
            //i think we get here when server disconnects us
			Com_Printf("Connection failed!");
			break;
        }
    }
}
/*
 ==================
 CL_Frame
 ==================
*/
void CL_Frame (int msec){

	int		time;

	if (cls.state == CS_UNINITIALIZED)
		return;		// Nothing running on the client

	if (com_speeds->integerValue)
		time = Sys_Milliseconds();

	// Lock the framerate and take screenshots for AVI demos
	if (cl_aviDemo->integerValue > 0){
		msec = 1000 / cl_aviDemo->integerValue;
		if (msec < 1)
			msec = 1;

		if (cls.state == CS_ACTIVE || cl_forceAviDemo->integerValue)
			Cmd_ExecuteText(CMD_EXEC_NOW, Str_VarArgs("screenShot -silent avi%08i.tga\n", cls.frameCount));
	}

	cls.frameTime += msec;
	cls.frameMsec = msec;
	cls.frameCount++;

	CL_ReadPackets();
	// Check if we need to send a user info update
	CL_CheckUserInfo();

	// Let the mouse activate/deactivate and move
	IN_MouseFrame();

	// Send intentions now
	CL_SendCommands();

	// Run the game simulation
	CG_RunFrame();

	// Update the screen
	CL_UpdateScreen();

	// Update sound
	S_Update(cls.frameTime);

	Con_RunConsole();

	if (com_speeds->integerValue)
		com_timeClient += (Sys_Milliseconds() - time);
}

/*
 ==================
 CL_Init
 ==================
*/
void CL_Init (void){

	if (com_dedicated->integerValue)
		return;		// Nothing running on the client

	Com_Printf("------- Client Initialization -------\n");

	cls.state = CS_DISCONNECTED;

	// Register our variables and commands
	cl_aviDemo = CVar_Get("cl_aviDemo", "0", CVAR_CHEAT, "Take screenshots for building an AVI demo");
	cl_forceAviDemo = CVar_Get("cl_forceAviDemo", "0", CVAR_CHEAT, "Take screenshots for building an AVI demo even if not in a game");
	cl_timeNudge = CVar_Get("cl_timeNudge", "0", CVAR_CHEAT, "Nudge time to add more or less latency");
	cl_showTimeDelta = CVar_Get("cl_showTimeDelta", "0", CVAR_CHEAT, "Show time delta");
	cl_showMessages = CVar_Get("cl_showMessages", "0", CVAR_CHEAT, "Show server messages");
	cl_showFPS = CVar_Get("cl_showFPS", "0", CVAR_ARCHIVE, "Show frames rendered per second");
	cl_playerName = CVar_Get("cl_playerName", "Unnamed Player", CVAR_ARCHIVE | CVAR_USERINFO, "Player name");
	cl_password = CVar_Get("cl_password", "", CVAR_USERINFO, "Password used when connecting");
	cl_rate = CVar_Get("cl_rate", "10000", CVAR_ARCHIVE | CVAR_USERINFO, "Network rate in bytes per second");
	cl_snapshots = CVar_Get("cl_snapshots", "30", CVAR_ARCHIVE | CVAR_USERINFO, "Requested snapshots per second");
	cl_maxPackets = CVar_Get("cl_maxPackets", "60", CVAR_ARCHIVE, "Maximum number of packets to send per second");;
	cl_packetDup = CVar_Get("cl_packetDup", "1", CVAR_ARCHIVE, "Number of duplicate packets to send");
	cl_allowDownload = CVar_Get("cl_allowDownload", "1", CVAR_ARCHIVE, "Allow file downloads from server");
	cl_deltaCompress = CVar_Get("cl_deltaCompress", "1", 0, "Delta compress network messages");
	cl_timeOut = CVar_Get("cl_timeOut", "180.0", 0, "Connection time-out time in seconds");
	cl_rconAddress = CVar_Get("cl_rconAddress", "", 0, "Remote console address");
	cl_rconPassword = CVar_Get("cl_rconPassword", "", 0, "Remote console password");

	in_freeLook = CVar_Get("in_freeLook", "1", CVAR_ARCHIVE, "Look around with mouse");
	in_alwaysRun = CVar_Get("in_alwaysRun", "0", CVAR_ARCHIVE, "Always run");
	in_pitchSpeed = CVar_Get("in_pitchSpeed", "140.0", CVAR_ARCHIVE, "Pitch speed when looking up/down");
	in_yawSpeed = CVar_Get("in_yawSpeed", "140.0", CVAR_ARCHIVE, "Yaw speed when turning right/left");
	in_angleSpeedKey = CVar_Get("in_angleSpeedKey", "1.5", CVAR_ARCHIVE, "Angle change speed when running");

	m_showRate = CVar_Get("m_showRate", "0", CVAR_CHEAT, "Show mouse rate");
	m_showMove = CVar_Get("m_showMove", "0", CVAR_CHEAT, "Show mouse movement");
	m_sensitivity = CVar_Get("m_sensitivity", "5.0", CVAR_ARCHIVE, "Mouse view sensitivity");
	m_smooth = CVar_Get("m_smooth", "1", CVAR_ARCHIVE, "Number of samples blended for mouse viewing");
	m_accel = CVar_Get("m_accel", "0.0", CVAR_ARCHIVE, "Mouse acceleration");
	m_pitch = CVar_Get("m_pitch", "0.022", CVAR_ARCHIVE, "Mouse pitch scale");
	m_yaw = CVar_Get("m_yaw", "0.022", CVAR_ARCHIVE, "Mouse yaw scale");
	m_strafe = CVar_Get("m_strafe", "6.25", CVAR_ARCHIVE, "Mouse strafe scale");

	Cmd_AddCommand("connect", CL_Connect_f, "Connects to a server");
	Cmd_AddCommand("disconnect", CL_Disconnect_f, "Disconnects from server");
	Cmd_AddCommand("userInfo", CL_UserInfo_f, "Shows user information");
	Cmd_AddCommand("dumpServerInfo", CL_DumpServerInfo_f, "Shows server information");
	Cmd_AddCommand("restart", CL_Restart_f, "Restarts the video subsystem");
	
	// Initialize console
	Con_Init();

	// Initialize renderer and sound system
	R_Init(true);
	S_Init(true);

	// Initialize input
	IN_Init();
	IN_AddCommands();

	// Initialize GUI
	GUI_Init();

	// Load local media
	CL_LoadMedia();

	//initialise network
	cls.client = enet_host_create (NULL /* create a client host */,
					1 /* only allow 1 outgoing connection */,
					57600 / 8 /* 56K modem with 56 Kbps downstream bandwidth */,
					14400 / 8 /* 56K modem with 14 Kbps upstream bandwidth */);
	Com_Printf("-------------------------------------\n");
}

/*
 ==================
 CL_Shutdown
 ==================
*/
void CL_Shutdown (void){

	if (cls.state == CS_UNINITIALIZED)
		return;		// Nothing running on the client

	Com_Printf("------- Client Shutdown -------\n");

	Cmd_RemoveCommand("connect");
	Cmd_RemoveCommand("reconnect");
	Cmd_RemoveCommand("disconnect");
	Cmd_RemoveCommand("checkUpdates");
	Cmd_RemoveCommand("localServers");
	Cmd_RemoveCommand("globalServers");
	Cmd_RemoveCommand("requestServerInfo");
	Cmd_RemoveCommand("requestServerStatus");
	Cmd_RemoveCommand("rcon");
	Cmd_RemoveCommand("userInfo");
	Cmd_RemoveCommand("dumpServerInfo");
	Cmd_RemoveCommand("restartVideo");
	Cmd_RemoveCommand("restartSound");
	Cmd_RemoveCommand("recordDemo");
	Cmd_RemoveCommand("stopRecording");
	Cmd_RemoveCommand("playDemo");
	Cmd_RemoveCommand("timeDemo");

	CL_Disconnect(false);

	GUI_Shutdown();

	IN_RemoveCommands();
	IN_Shutdown();

	S_Shutdown(true);
	R_Shutdown(true);

	Con_Shutdown();

	memset(&cls, 0, sizeof(clientStatic_t));

	Com_Printf("-------------------------------\n");
}
